Skip to content

txnprovider/txpool: wake a caller that goes away while waiting for a block - #23343

Open
lystopad wants to merge 2 commits into
mainfrom
feature/lystopad/txpool-best-cancellable-wait
Open

txnprovider/txpool: wake a caller that goes away while waiting for a block#23343
lystopad wants to merge 2 commits into
mainfrom
feature/lystopad/txpool-best-cancellable-wait

Conversation

@lystopad

Copy link
Copy Markdown
Member

Second prerequisite for #23272, after #23333. Raised by @yperbasis reviewing that PR.

TxPool.best parks in p.lastSeenCond.Wait() until the block it was asked to build on top of arrives. sync.Cond has no notion of a context: the wait ends when a new block broadcasts the condition (pool.go:361) or when the node shuts down. Cancelling the caller does nothing.

So a caller that has given up stays parked, holding its read transaction and SharedDomains, until a block arrives. While the chain is stalled that is never — which is exactly when builders accumulate and when releasing them matters.

#23333 stopped a cancelled caller leaving the pool lock held. This is the other half: making the wait notice at all.

The fix

Cancellation broadcasts the condition, so the parked caller wakes, sees its context, and returns. Waiters re-check their own condition on waking regardless, so an extra broadcast is harmless.

The broadcast takes the pool lock. That is load-bearing rather than incidental: without it the broadcast could land between the cancellation check and Wait(), and the wakeup would be lost — the caller would sleep on exactly as before. Holding the lock means the broadcast can only happen before the check, where the check sees it, or after Wait() has released the lock, where the broadcast reaches it.

The watcher goroutine is scoped to the call and exits with it.

Test

TestBestReturnsWhenItsCallerGoesAwayWhileWaitingForABlock cancels only once the caller has reached the "Waiting for block" trace, so it exercises the wait rather than the check in front of it. Nothing else wakes it: no block arrives, nothing shuts down. Against the current code it fails with best never returned; only a new block would have woken it.

Why separate

It is a txnprovider/txpool change, and #23272 is an execution/ change that is large already. Same reasoning as #23333, which @yperbasis suggested splitting out for the same reason.

Note: make lintci reports db/seg/decompress.go:199: field residencyOnce is unused, which is darwin-only — that field's user is residency_gate_linux.go, so CI does not see it. golangci-lint is clean for txnprovider/txpool/....

…block

best parks in a sync.Cond, which has no notion of a context: the wait ends when a new block
broadcasts the condition, or when the node shuts down. A caller that gave up in the meantime
therefore stays parked, holding whatever it brought with it, until one of those happens. While
the chain is stalled that is never, which is when it matters most.

Cancelling now broadcasts, so the caller wakes and returns. The broadcast takes the pool lock,
which is what keeps it from landing between the cancellation check and the wait, where a wakeup
would be lost.

Reachable once anything cancels a live build: discarding an evicted payload builder does, which
is why this is a prerequisite for that change rather than part of it.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@yperbasis yperbasis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cancellation watcher is not joined before best returns. Closing waitDone only makes one select case ready; when ctx is already cancelled, the watcher may still choose the ctx branch and acquire p.lock after the call has returned. This makes TestBestReleasesTheLockWhenTheCallerGivesUpWaitingForABlock flaky at its TryLock assertion and means the watcher is not scoped to the call as described.

I reproduced it with:

GOMAXPROCS=4 go test -race ./txnprovider/txpool -run TestBestReleasesTheLockWhenTheCallerGivesUpWaitingForABlock -count=1000

Please synchronize watcher completion before returning, after any held pool lock has been released, or restructure the watcher so it cannot touch p.lock after best returns.

Closing its stop channel only made one of the watcher's two cases ready. With the context
already ended it could pick the other, and take the pool lock after the call it belongs to had
returned. Waiting for it to finish keeps it scoped to the call, as described. The wait is
registered before the lock is taken, so it runs after the lock is released.
@lystopad

Copy link
Copy Markdown
Member Author

Fixed in 570d0d7a9f.

You are right on both counts. Closing the stop channel only made one of the watcher's two cases ready, so with the context already ended it could pick the other and take the pool lock after the call had returned — which makes the flake real and makes "scoped to the call" untrue as I had described it.

The watcher is joined now rather than merely told to stop. The join is registered before the lock is taken, so deferred order puts it after the lock is released and it cannot deadlock against the goroutine it is waiting for.

Your reproduction passes: GOMAXPROCS=4 go test -race ./txnprovider/txpool -run TestBestReleasesTheLockWhenTheCallerGivesUpWaitingForABlock -count=1000. Removing the join and rerunning it fails within the first handful of iterations with best returned holding the pool lock, so the existing test does catch this once the run is long enough — thank you for running it that way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants